using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Runtime.Serialization;
using System.Security.Permissions;
using InfiniTec.Runtime.Serialization;
using InfiniTec.Threading;
using JetBrains.Annotations;
using Trace=InfiniTec.Diagnostics.Trace;

namespace InfiniTec.Exchange.Notifications
{
	/// <summary>
	/// Represents a Push subscription for a folder on an Exchange 2007 server or later.
	/// </summary>
	/// <remarks>
	/// <para>When the subscription is active, it will be notified by the Exchange server. If the subscription times out for some reason, the 
	/// instance will try to recreate it on the server. If this fails, the <see cref="RenewalFailed"/> event will be raised.</para>
	/// </remarks>
	[Serializable]
	[KnownType(typeof (List<SubscriptionStatus>))]
	[KnownType(typeof (EventTypes))]
	[KnownType(typeof (List<FolderReference>))]
	[KnownType(typeof (SubscriptionStatus))]
	public sealed class Subscription : ISerializable
	{
		private static readonly Trace _Trace = SubscriptionCollection.Trace;
		private readonly EventTypes _Events;
		private readonly ReadOnlyCollection<FolderReference> _Folders;
		private readonly object _PollLock = new object();

		private readonly object _TimeoutLock = new object();
		private DateTime _LastNotification;
		private PushNotificationListener _Listener;
		private DispatcherCookie _PollCallbackCookie;
		private int _PollIntervalInMinutes = 5;
		private SubscriptionStatus _Status = SubscriptionStatus.NotSubscribed;
		private DispatcherCookie _TimeoutCookie;
		private int _TimeoutIntervalInMinutes = 30;
		private SubscriptionType _Type;

		/// <summary>
		/// Initializes a new instance of the <see cref="Subscription"/> class.
		/// </summary>
		/// <param name="folders">A list of references to folders to monitor for changes.</param>
		/// <param name="events">A valid combination of the <see cref="EventTypes"/> values, except for <see cref="EventTypes.None"/>.</param>
		/// <param name="adapter">An instance which implements the <see cref="IExchangeWebServicesAdapter"/>, e.g. the <see cref="ExchangeWebServicesAdapter"/>.</param>
		/// <param name="type">Specifies the type of the subscription. If set to <see cref="SubscriptionType.Push"/>, a <see cref="PushNotificationListener"/> also needs to be speciefied. Valid values are <see cref="SubscriptionType.Push"/> and <see cref="SubscriptionType.Pull"/>.</param>
		/// <param name="listener">The listener used to receive callbacks.</param>
		internal Subscription(IEnumerable<FolderReference> folders, EventTypes events, IExchangeWebServicesAdapter adapter, SubscriptionType type,
		                      PushNotificationListener listener)
		{
			if (folders == null) throw new ArgumentNullException("folders");
			if (adapter == null) throw new ArgumentNullException("adapter");
			
			if (type == SubscriptionType.Push &&  listener == null) throw new ArgumentNullException("listener", "The listener variable must not be null when creating a push subscription.");

			if (type != SubscriptionType.Pull && type != SubscriptionType.Push)
				throw new InvalidEnumArgumentException("type", (int) type, typeof (SubscriptionType));

			if (events == EventTypes.None || (events & EventTypes.All) != events)
				throw new InvalidEnumArgumentException("events", (int) events, typeof (EventTypes));

			_Folders = new ReadOnlyCollection<FolderReference>(new List<FolderReference>(folders));
			_Events = events;
			_Listener = listener;
			ExchangeWebServicesAdapter = adapter;
			Type = type;
		}

		/// <summary>
		/// Initializes a new instance of the <see cref="Subscription"/> class. Called during deserialisation.
		/// </summary>
		/// <param name="info">The info.</param>
		/// <param name="context">The context.</param>
		private Subscription(SerializationInfo info, StreamingContext context)
		{
			Id = info.GetString("Id");
			Watermark = info.GetString("Watermark");
			_Events = info.GetValue<EventTypes>("Events");
			var folders = info.GetValue<List<FolderReference>>("Folders");
			_Folders = new ReadOnlyCollection<FolderReference>(folders);
			_LastNotification = info.GetDateTime("LastNotification");
			_TimeoutIntervalInMinutes = info.GetInt32("TimeoutIntervalInMinutes");
			_Status = info.GetValue<SubscriptionStatus>("Status");
			if (_Status == SubscriptionStatus.Active)
			{
				IsRestartNeeded = true;
			}
			_Status = SubscriptionStatus.NotSubscribed;

			try
			{
				Type = info.GetValue<SubscriptionType>("SubscriptionType");
			}
			catch (SerializationException)
			{
				// this is ok - Type may not exist 
				Type = SubscriptionType.Push;
			}
		}

		/// <summary>
		/// Gets or sets the notification type used - push or pull. Can only be set if the subscription instance has not already registered with notification listener.
		/// </summary>
		/// <value>The type.</value>
		[PublicAPI]
		public SubscriptionType Type
		{
			get { return _Type; }
			private set
			{
				EnsureInstanceIsNotRegistered("Type");
				_Type = value;
			}
		}

		/// <summary>
		/// Switches the type of the subscription either from push to pull or vice versa.
		/// </summary>
		/// <remarks>
		/// <para>Switching the subscription type stops and restarts the subscription, if the <see cref="Status"/> is <see cref="SubscriptionStatus.Active"/>.</para>
		/// <para>When switching the subscription type, all events not yet received are lost, as the subscription is cancelled and a new subscription is created.</para>
		/// </remarks>
		/// <param name="newSubscriptionType">New type of the subscription.</param>
		[PublicAPI]
		public void SwitchSubscriptionType(SubscriptionType newSubscriptionType)
		{
			if (newSubscriptionType != SubscriptionType.Pull && newSubscriptionType != SubscriptionType.Push)
			{
				throw new InvalidEnumArgumentException("newSubscriptionType", (int) newSubscriptionType, typeof(SubscriptionType));
			}
			if (Type == newSubscriptionType)
			{
				return;
			}
			bool wasRunning = false;
			if (Status == SubscriptionStatus.Active)
			{
				wasRunning = true;
				Stop();
			}
			Type = newSubscriptionType;
			Watermark = null;

			if (wasRunning)
			{
				Start();
			}
		}

		/// <summary>
		/// Gets the folders monitored by this instance.
		/// </summary>
		/// <value>The folders.</value>
		public IEnumerable<FolderReference> Folders
		{
			get { return _Folders; }
		}

		/// <summary>
		/// Gets the types of events this subscription monitors.
		/// </summary>
		/// <value>The events.</value>
		[PublicAPI]
		public EventTypes Events
		{
			get { return _Events; }
		}

		/// <summary>
		/// Gets the date of the last notification received.
		/// </summary>
		/// <value>The last notification.</value>
		[PublicAPI]
		public DateTime LastNotification
		{
			get { return _LastNotification; }
		}

		/// <summary>
		/// Gets or sets the exchange web services adapter used to call to the Exchange 2007 CAS role.
		/// </summary>
		/// <value>An instance which implements the <see cref="IExchangeWebServicesAdapter"/>, e.g. the <see cref="ExchangeWebServicesAdapter"/>.</value>
		[PublicAPI]
		internal IExchangeWebServicesAdapter ExchangeWebServicesAdapter { get; set; }

		/// <summary>
		/// Gets or sets a value indicating whether this instance needs to be restarted after deserialization.
		/// </summary>
		/// <value>
		/// 	<c>true</c> if this instance is needs to be restarted; otherwise, <c>false</c>.
		/// </value>
		internal bool IsRestartNeeded { get; private set; }

		/// <summary>
		/// Gets the current status of the subscription.
		/// </summary>
		/// <value>The status.</value>
		[PublicAPI]
		public SubscriptionStatus Status
		{
			get { return _Status; }
		}

		/// <summary>
		/// Gets or sets the id for this subscription.
		/// </summary>
		/// <value>The id.</value>
		[PublicAPI]
		internal string Id { get; set; }

		/// <summary>
		/// Gets or sets the watermark which represents the current state of the monitored folder.
		/// </summary>
		/// <value>The watermark.</value>
		[PublicAPI]
		internal string Watermark { get; set; }

		/// <summary>
		/// Gets or sets the timeout interval in minutes. A subscription is considered to be timed-out if no notification is received after twice
		/// the timeout interval. Values must be between 1 and 1440.
		/// </summary>
		/// <value>The timeout interval in minutes. It must been between 1 and 1440.</value>
		[PublicAPI]
		public int TimeoutIntervalInMinutes
		{
			get { return _TimeoutIntervalInMinutes; }
			set
			{
				EnsureInstanceIsNotRegistered("TimeoutIntervalInMinutes");
				if (value < 1 || value > 1440) throw new ArgumentOutOfRangeException("value", value, "The value must be between 1 and 1440");
				_TimeoutIntervalInMinutes = value;
			}
		}

		/// <summary>
		/// Gets or sets the poll interval in minutes. Only valid on pull subscriptions.
		/// Values must be between 1 and 1440. The subscription will poll the Exchange server 
		/// for modification each time the poll interval elapsed.
		/// </summary>
		/// <value>The timeout interval in minutes. It must been between 1 and 1440.</value>
		[PublicAPI]
		public int PollIntervalInMinutes
		{
			get { return _PollIntervalInMinutes; }
			set
			{
				EnsureInstanceIsNotRegistered("PollIntervalInMinutes");
				if (value < 1 || value > 1440) throw new ArgumentOutOfRangeException("value", value, "The value must be between 1 and 1440");
				_PollIntervalInMinutes = value;
			}
		}

		#region ISerializable Members

		/// <summary>
		/// Populates a <see cref="T:System.Runtime.Serialization.SerializationInfo"/> with the data needed to serialize the target object.
		/// </summary>
		/// <param name="info">The <see cref="T:System.Runtime.Serialization.SerializationInfo"/> to populate with data.</param>
		/// <param name="context">The destination (see <see cref="T:System.Runtime.Serialization.StreamingContext"/>) for this serialization.</param>
		/// <exception cref="T:System.Security.SecurityException">The caller does not have the required permission. </exception>
		[SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.SerializationFormatter)]
		void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context)
		{
			info.AddValue("Id", Id);
			info.AddValue("Watermark", Watermark);
			info.AddValue("Events", _Events);
			info.AddValue("Folders", new List<FolderReference>(_Folders));
			info.AddValue("LastNotification", _LastNotification);
			info.AddValue("TimeoutIntervalInMinutes", _TimeoutIntervalInMinutes);
			info.AddValue("Status", _Status);
			info.AddValue("SubscriptionType", Type);
		}

		#endregion

		internal void SetListener(PushNotificationListener value)
		{
			_Listener = value;
		}

		/// <summary>
		/// Occurs when the renewal of the subscription after it has timed-out fails.
		/// </summary>
		[field: NonSerialized]
		[PublicAPI]
		public event EventHandler<ExceptionEventArgs> RenewalFailed;

		private void EnsureInstanceIsNotRegistered(string propertyName)
		{
			if (string.IsNullOrEmpty(propertyName)) throw new ArgumentNullException("propertyName");
			if (_Status != SubscriptionStatus.NotSubscribed)
				throw new InvalidOperationException(
				string.Format(CultureInfo.CurrentCulture,
				              "The property {0} cannot be set after the subscriptions has been started.", propertyName));
		}

		/// <summary>
		/// Occurs when a new mail arrives in of the folders being monitored.
		/// </summary>
		[field: NonSerialized]
		[PublicAPI]
		public event EventHandler<ItemNotificationEventArgs> NewMail;

		/// <summary>
		/// Occurs when a new item has been created in of the folders being monitored.
		/// </summary>
		[field: NonSerialized]
		[PublicAPI]
		public event EventHandler<ItemNotificationEventArgs> ItemCreated;

		/// <summary>
		/// Occurs when an item has been copied from one folder to another.
		/// </summary>
		[field: NonSerialized]
		[PublicAPI]
		public event EventHandler<CopyOrMoveNotificationEventArgs> ItemCopied;

		/// <summary>
		/// Occurs when an item has been deleted in of the folders being monitored.
		/// </summary>
		[field: NonSerialized]
		[PublicAPI]
		public event EventHandler<ItemNotificationEventArgs> ItemDeleted;

		/// <summary>
		/// Occurs when a folder has been modified. 
		/// </summary>
		[field: NonSerialized]
		[PublicAPI]
		public event EventHandler<FolderModifiedNotificationEventArgs> FolderModified;

		/// <summary>
		/// Occurs when an item has been modified in one of the folders being monitored.
		/// </summary>
		[field: NonSerialized]
		[PublicAPI]
		public event EventHandler<ItemNotificationEventArgs> ItemModified;

		/// <summary>
		/// Occurs when an item has been moved from one folder to another.
		/// </summary>
		[field: NonSerialized]
		[PublicAPI]
		public event EventHandler<CopyOrMoveNotificationEventArgs> ItemMoved;

		/// <summary>
		/// Activates this subscription by creating a subscription through the <see cref="ExchangeWebServicesAdapter"/> on an Exchange 2007 CAS role.
		/// </summary>
		[PublicAPI]
		public void Start()
		{
			if (Status == SubscriptionStatus.Active)
			{
				return;
			}
			EnsureAdapterIsSpecified();

			using (_Trace.StartOperation("Create subscription"))
			{
				try
				{
					SubscriptionInfo info;
					if (Type == SubscriptionType.Push)
					{
						if (_Listener == null)
							throw new InvalidOperationException("A PushNotificationLister must be associated with this instance to use push subscriptions.");
						info = ExchangeWebServicesAdapter.Subscribe(_Folders, _Events, Watermark, TimeoutIntervalInMinutes,
						                                            _Listener.LocalEndpoint);
					}
					else
					{
						info = ExchangeWebServicesAdapter.Subscribe(_Folders, _Events, Watermark, TimeoutIntervalInMinutes);
					}
					Id = info.Id;
					Watermark = info.InitialWatermark;
					_Status = SubscriptionStatus.Active;

					if (Type == SubscriptionType.Pull)
					{
						PollSubscriptionForChanges();
					}

					_Listener.Register(this);
					UpdateTimeoutCallback();

					_Status = SubscriptionStatus.Active;
					_Trace.Verbose("Subscription was created sucessfully. Details are listed in the following event.");
					_Trace.Data(TraceEventType.Verbose, 0, new[]
					                                       {
					                                       string.Format(CultureInfo.InvariantCulture, "Id: {0}", Id),
					                                       string.Format(CultureInfo.InvariantCulture, "Watermark: {0}", Watermark)
					                                       });
				}
				catch (Exception ex)
				{
					_Trace.Error("Failed to create a subscription. Error: {0}", ex);
					throw;
				}
			}
		}

		private void EnsureAdapterIsSpecified()
		{
			if (ExchangeWebServicesAdapter == null)
			{
				throw new InvalidOperationException("The property ExchangeWebServicesAdapter is required for this operation.");
			}
		}

		private void StopTimer()
		{
			_Trace.Verbose("Canelling timeout callback for this subscription.");
			lock (_TimeoutLock)
			{
				if (_TimeoutCookie == null) return;
				Dispatcher.CancelCallback(_TimeoutCookie);
				_TimeoutCookie = null;
			}
		}

		private static void RaiseHandler(Action action)
		{
			if (action == null) throw new ArgumentNullException("action");
			try
			{
				action();
			}
			catch (Exception ex)
			{
				HandleEventHandlerException(ex);
			}
		}

		internal void ProcessEvents(IEnumerable<EventInfo> events)
		{
			if (events == null) throw new ArgumentNullException("events");
			using (_Trace.StartOperation("Processing notification for subscription {0}", Id))
			{
				_LastNotification = DateTime.Now;
				UpdateTimeoutCallback();

				var previousWatermark = Watermark;

				foreach (var eventInfo in events)
				{
					using (_Trace.StartOperation("Processing notification of type {0}", eventInfo.EventType))
					{
						if (eventInfo.Watermark == null)
						{
							throw new InvalidOperationException("The Watermark property of the eventInfo must not be null.");
						}
						Watermark = eventInfo.Watermark;
						_Trace.Verbose("Updating watermark. Both watermarks are logged in the following entry.");
						_Trace.Data(TraceEventType.Verbose, 0, new[]
						                                       {
						                                       string.Format(CultureInfo.InvariantCulture, "New watermark: {0}",
						                                                     Watermark),
						                                       string.Format(CultureInfo.InvariantCulture, "Previous watermark: {0}",
						                                                     previousWatermark)
						                                       });

						switch (eventInfo.EventType)
						{
							case EventType.Status:
								continue;
							case EventType.Copied:
							case EventType.Moved:
								ProcessCopyOrMoveEvent(eventInfo);
								break;
							case EventType.Modified:
								ProcessModificationEvent(eventInfo);
								break;
							case EventType.Created:
							case EventType.Deleted:
							case EventType.NewMail:
								ProcessBaseEvent(eventInfo);
								break;
						}
					}
				}
			}
		}

		private void ProcessBaseEvent(EventInfo eventInfo)
		{
			EventHandler<ItemNotificationEventArgs> basehandler = null;

			switch (eventInfo.EventType)
			{
				case EventType.Created:
					basehandler = ItemCreated;
					break;
				case EventType.Deleted:
					basehandler = ItemDeleted;
					break;
				case EventType.NewMail:
					basehandler = NewMail;
					break;
			}
			if (basehandler != null)
			{
				RaiseHandler(() => basehandler.Raise(this, new ItemNotificationEventArgs
														   {
															   ParentFolder = eventInfo.ParentFolderId,
															   ItemId = eventInfo.ItemId,
															   Timestamp = eventInfo.Timestamp
														   }));
			}
		}

		private void ProcessModificationEvent(EventInfo eventInfo)
		{
			switch (eventInfo.EventCategory)
			{
				case EventCategory.FolderEvent:
					RaiseHandler(() => FolderModified.Raise(this, new FolderModifiedNotificationEventArgs
																  {
																	  ParentFolder = eventInfo.ParentFolderId,
																	  Timestamp = eventInfo.Timestamp,
																	  UnreadCount = eventInfo.UnreadCount
																  }));
					break;
				case EventCategory.ItemEvent:
					RaiseHandler(() => ItemModified.Raise(this, new ItemNotificationEventArgs
																{
																	ParentFolder = eventInfo.ParentFolderId,
																	ItemId = eventInfo.ItemId,
																	Timestamp = eventInfo.Timestamp
																}));
					break;
			}
		}

		private void ProcessCopyOrMoveEvent(EventInfo eventInfo)
		{
			var copiedOrMovedHandler = eventInfo.EventType == EventType.Copied
			                           ? ItemCopied
			                           : ItemMoved;

			RaiseHandler(() => copiedOrMovedHandler.Raise(this, new CopyOrMoveNotificationEventArgs
																{
																	ParentFolder = eventInfo.ParentFolderId,
																	ItemId = eventInfo.ItemId,
																	Timestamp = eventInfo.Timestamp,
																	OldItemId = eventInfo.OldItemId,
																	OldParentFolder = eventInfo.OldParentFolderId
																}));
		}

		private static void HandleEventHandlerException(Exception exception)
		{
			if (exception == null) throw new ArgumentNullException("exception");
			_Trace.Error("The handler of a subscription caused the following exception: {0}", exception);
		}

		private void UpdateTimeoutCallback()
		{
			lock (_TimeoutLock)
			{
				_LastNotification = DateTime.Now;
				if (_TimeoutCookie != null)
				{
					Dispatcher.CancelCallback(_TimeoutCookie);
				}

				_TimeoutCookie = Dispatcher.RegisterCallback(_LastNotification + TimeSpan.FromMinutes(TimeoutIntervalInMinutes*2),
				                                             state =>
				                                             {
				                                             	_TimeoutCookie = null;
				                                             	TimeoutCallback();
				                                             });
			}
		}

		private void UpdatePollCallback()
		{
			lock (_PollLock)
			{
				_LastNotification = DateTime.Now;
				if (_PollCallbackCookie != null)
				{
					Dispatcher.CancelCallback(_PollCallbackCookie);
				}

				_PollCallbackCookie = Dispatcher.RegisterCallback(_LastNotification + TimeSpan.FromMinutes(PollIntervalInMinutes),
																   state =>
																   {
																   	_PollCallbackCookie = null;
																   	PollSubscriptionForChanges();
																   });
			}
		}

		private void PollSubscriptionForChanges()
		{
			if (Status != SubscriptionStatus.Active) return;

			using (_Trace.StartOperation("Polling events for subscription {0}", Id))
			try
			{
				var events = ExchangeWebServicesAdapter.GetEvents(Id, Watermark);
				ProcessEvents(events);
			}
			catch (Exception ex)
			{
				_Trace.Error("Failed to poll the subscription for events. Error: {0}", ex);
			}
			finally
			{
				UpdatePollCallback();
			}
		}

		[SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
		private void TimeoutCallback()
		{
			using (_Trace.StartOperation("Subscription timed out"))
			{
				try
				{
					lock (_TimeoutLock)
					{
						_TimeoutCookie = null;
					}
					Id = null;
					_Trace.Information("Subscription has timed out. Creating a new one for the current set of folders.");
					Start();
					_Trace.Verbose("Subscription renewal was successful.");
				}
				catch (InvalidOperationException)
				{
					_Trace.Warning(
					"The subscription could not be renewed, because the associated SubscriptionManger has not been started.");
				}
				catch (Exception ex)
				{
					_Trace.Error("Failed to renew the subscription. Erorr: {0}", ex);
					RenewalFailed.Raise(this, new ExceptionEventArgs(ex));
				}
			}
		}

		internal void Stop()
		{
			if (Type == SubscriptionType.Pull)
			{
				ExchangeWebServicesAdapter.Unsubscribe(Id);
			}
			_Status = SubscriptionStatus.NotSubscribed;
			StopTimer();
		}
	}
}